/*
 * Twanoh Library, a useful Android utility library.
 * Copyright (C) 2013 Richard Schilling
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package gnu.org.twanoh.statemachine;

import java.util.EnumSet;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Implements a simple state machine.
 * 
 * @author rschilling
 */
public class StateMachine<T extends Enum<T>> {

    /**
     * The current state.
     */
    private T state;

    /**
     * The start state that this machine gets reset to.
     */
    private final T startState;

    /**
     * The map of valid state transitions.
     */
    private final StateMap<T> map;

    /**
     * A new state machine.
     * 
     * @param map the map to use for state transition validation.
     */
    public StateMachine(StateMap<T> map, T startState) {

        if (map == null)
            throw new IllegalArgumentException("map cannot be null.");

        if (startState == null)
            throw new IllegalArgumentException("startState cannot be null");

        if (map.containsKey(startState))
            throw new IllegalArgumentException("start state is not found in the map.");

        this.map = map;
        this.state = startState;
        this.startState = startState;

    }

    /**
     * Transition from the current state to the specified state.
     * 
     * @param to the state to transfer to.
     * @throws IllegalArgumentException if the transition is invalid.
     */
    public final void transition(T to) {
        if (!map.isValidTransition(state, to))
            throw new IllegalArgumentException(String.format(
                    "Transitioning from state %s to %s is invalid.", state.toString(),
                    to.toString()));
        state = to;
    }

    public final void transition(T to, StringBuilder logOutput) {
        T savedState = state;
        transition(to);
        logOutput.append(String.format("Transitioned from %s to %s", savedState.toString(),
                state.toString()));
    }

    /**
     * Returns the current state.
     * 
     * @return the current state.
     */
    public final T currentState() {
        return state;
    }

    /**
     * Resets the state machine to the start state.s
     */
    public final void reset() {
        state = startState;

    }

    /**
     * Returns a list of states that be used to transition to state s. If the no
     * transitions are found null is returned. If a non-null value is returned,
     * the list is guaranteed to have at least one item.
     * 
     * @param s the state to transition to.
     * @return the list of states that can be used to transition to s.
     */
    public EnumSet<T> canTransitionTo(T s) {
        if (!map.containsKey(s))
            return null;

        return map.get(s);

    }

    /**
     * Returns a list of destination states that can be used to transition from
     * state s. If no states are found null is returned.
     * 
     * @param s the state to transition from.
     * @return the list of states that can be reached from s.
     */
    public EnumSet<T> canTransitionFrom(T s) {

        @SuppressWarnings("unchecked")
        EnumSet<T> result = EnumSet.noneOf(s.getClass());

        Set<Entry<T, EnumSet<T>>> kSet = map.entrySet();

        for (Entry<T, EnumSet<T>> e : kSet) {
            if (e.getValue().contains(s))
                result.add(e.getKey());
        }

        if (result.size() == 0)
            return null;

        return result;
    }
}
